package com.biometric.core

import android.util.Log
import androidx.annotation.CheckResult
import androidx.annotation.Keep
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.fragment.app.FragmentActivity
import com.biometric.core.authenticator.AuthenticateListenerAdapter
import com.biometric.core.authenticator.impl.CredentialAuthenticator
import com.biometric.core.fallback.FallbackStrategy
import com.biometric.core.fallback.FallbackStrategy.Companion.MeetMaxAttempt
import com.biometric.core.fallback.FallbackStrategy.Companion.OnError
import com.wei.android.lib.fingerprintidentify.Authenticator
import com.wei.android.lib.fingerprintidentify.Authenticator.AuthenticateListener

@Keep
class AuthClient private constructor(
    private val activity: FragmentActivity,
    private val maxAttemptPerAuthenticator: Int,
    authenticateListener: AuthenticateListener,
    primaryAuthenticator: Authenticator,
    private val exceedMaxAttemptCallback: ReachMaxAttemptCallback?,
    private val credentialAuthForBiometricLockout: Boolean,
    private val promptTitle: String,
    private val fallbackAuthenticator: Authenticator?,
    @FallbackStrategy
    private val authenticatorFallbackStrategy: Int,
    private val biometricLockout: BiometricLockout
) {
    private val TAG = "AuthClient"
    private var attemptedTimes = 0
    private var currAuthenticator: Authenticator = primaryAuthenticator
    private val internalAuthenticatorListener = AuthenticatorListenerWrapper(authenticateListener)
    private val authenticatorAttemptMap = hashMapOf<Authenticator, Int>()

    init {
        authenticatorAttemptMap[primaryAuthenticator] = 0
        if (fallbackAuthenticator != null)
            authenticatorAttemptMap[fallbackAuthenticator] = 0
    }

    private val lockOutCode =
        arrayOf(BiometricPrompt.ERROR_LOCKOUT, BiometricPrompt.ERROR_LOCKOUT_PERMANENT)

    inner class AuthenticatorListenerWrapper(private val listener: AuthenticateListener) :
        AuthenticateListener {
        override fun onSuccess() {
            listener.onSuccess()
        }

        override fun onFailed() {
            listener.onFailed()
            authenticatorAttemptMap[currAuthenticator] =
                authenticatorAttemptMap.getOrElse(currAuthenticator) { 5 } + 1
            if (authenticatorAttemptMap.getOrElse(currAuthenticator) { 5 } >= maxAttemptPerAuthenticator) {
                exceedMaxAttemptCallback?.onReach(currAuthenticator)
            }
            doFallback(false)
        }

        override fun onError(code: Int, errMsg: CharSequence) {
            listener.onError(code, errMsg)
            doFallback(true)
            lockoutIfLockError(code)
            if (biometricLockout.isBiometricLockout() && credentialAuthForBiometricLockout){
                startCredentialAuthentication()
            }
        }

        override fun onCancelled() {
            listener.onCancelled()
        }
    }

    private fun lockoutIfLockError(code: Int) {
        val isLockout = code in lockOutCode
        if (isLockout) biometricLockout.systemLockout()
    }

    /**
     * 执行兜底验证器
     */
    private fun doFallback(hasError: Boolean) {
        if (fallbackAuthenticator == null) return
        // already using fallback
        if (currAuthenticator == fallbackAuthenticator) return
        val failAttempt = authenticatorAttemptMap[currAuthenticator] ?: 0
        val doFallback =
            (hasError && authenticatorFallbackStrategy == OnError)
                    || (failAttempt >= maxAttemptPerAuthenticator && authenticatorFallbackStrategy == MeetMaxAttempt)
        if (doFallback) {
            currAuthenticator = fallbackAuthenticator
            fallbackAuthenticator.authenticate(internalAuthenticatorListener)
        }
    }

    private fun startCredentialAuthentication() {
        if (fallbackAuthenticator is CredentialAuthenticator){
            currAuthenticator = fallbackAuthenticator
            fallbackAuthenticator.authenticate(internalAuthenticatorListener)
            return
        }
        val promptInfo = BiometricPrompt.PromptInfo.Builder()
            .setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
            .setTitle(promptTitle)
            .build()

        BiometricPrompt(activity, object : BiometricPrompt.AuthenticationCallback() {
            override fun onAuthenticationFailed() {
                super.onAuthenticationFailed()
                Log.w(TAG, "onAuthenticationFailed: PIN/图案解锁失败")
            }

            override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                super.onAuthenticationError(errorCode, errString)
                Log.w(TAG, "onAuthenticationFailed: PIN/图案解锁错误")
            }
        }).authenticate(promptInfo)

    }

    interface ReachMaxAttemptCallback {

        fun onReach(authenticator: Authenticator)
    }

    fun startAuthenticate() {
        attemptedTimes = 0
        currAuthenticator.authenticate(internalAuthenticatorListener)
    }

    /**
     * [activity] 这里的上下文只有在调用[credentialAuthenticateWhileBiometricLockout]的时候才需要，内部会创建 [BiometricPrompt] 使用到这个上下文
     */
    class Builder(private val activity: FragmentActivity) {

        private companion object {
            const val DEFAULT_MAX_ATTEMPT = 5
        }

        private var lockout: BiometricLockout = BiometricLockout()
        @FallbackStrategy
        private var authenticatorFallbackStrategy: Int = OnError
        private var fallbackAuthenticator: Authenticator? = null
        private var biometricPromptTitle: String = ""
        private var credentialAuthenticateForBiometricLockout: Boolean = false
        private var reachMaxAttemptCallback: ReachMaxAttemptCallback? = null
        private var maxAttemptPerAuthenticator = DEFAULT_MAX_ATTEMPT
        private var authenticateListener: AuthenticateListener = AuthenticateListenerAdapter()
        private lateinit var primaryAuthenticator: Authenticator


        /**
         *每个验证器能够允许的最大失败次数。
         **/
        @CheckResult
        fun maxAttemptPerAuthenticator(maxAttempt: Int): Builder {
            check(maxAttempt > 0) { "maxAttempt 必须大于 0 ，当前值：$maxAttempt" }
            this.maxAttemptPerAuthenticator = maxAttempt
            return this
        }

        /**
         * 验证器验证流程中的回调
         * onFail
         * onError
         * onSuccess
         * */
        @CheckResult
        fun listener(authenticateListener: AuthenticateListener): Builder {
            this.authenticateListener = authenticateListener
            return this
        }

        /**
         * 设置超过允许最大失败次数回调
         */
        @CheckResult
        fun setReachMaxAttemptCallback(callBack: ReachMaxAttemptCallback?): Builder {
            this.reachMaxAttemptCallback = callBack
            return this
        }

        /**
         * 如果触发系统失败次数，调用系统PIN/图案解锁
         * [title] 使用系统密码解锁时需要设置的标题,不能为空
         */
        @CheckResult
        fun credentialAuthenticateWhileBiometricLockout(
            title: String = ""
        ): Builder {
            this.credentialAuthenticateForBiometricLockout = true
            this.biometricPromptTitle = title
            return this
        }


        /**
         * 使用特定验证器进行解锁
         */
        @CheckResult
        fun authenticator(authenticator: Authenticator): Builder {
            this.primaryAuthenticator = authenticator
            return this
        }

        /**
         * 设置兜底验证器。如果 primaryAuthenticator无法进行验证，那么应急验证器会再次进行验证
         * 比如指纹识别验证失败太多，如果设置了密码/图案验证器，则会进行密码/图案验证
         */
        @CheckResult
        fun fallback(authenticator: Authenticator): Builder {
            this.fallbackAuthenticator = authenticator
            return this
        }

        /**
         * 应急验证器调用策略 值必须是[FallbackStrategy]的子类：
         *[OnError]、[MeetMaxAttempt]
         */
        @CheckResult
        fun fallbackStrategy(@FallbackStrategy strategy: Int): Builder {
            this.authenticatorFallbackStrategy = strategy
            return this
        }

        @CheckResult
        fun lockout(lockout: BiometricLockout): Builder {
            this.lockout = lockout
            return this
        }

        fun build(): AuthClient {
            // construct primary authenticator and fallback authenticator
            return AuthClient(
                activity,
                maxAttemptPerAuthenticator,
                authenticateListener,
                primaryAuthenticator,
                reachMaxAttemptCallback,
                credentialAuthenticateForBiometricLockout,
                biometricPromptTitle,
                fallbackAuthenticator,
                authenticatorFallbackStrategy,
                lockout
            )
        }
    }
}